import { Injectable } from '@angular/core';
import { AbstractControl, NgModel, ValidatorFn } from '@angular/forms';

@Injectable()
export class ValidatorFactoryService {
    constructor() { }

    /**
     * Gets the ForbiddenValueValidator to check whether the input contains the restrict value.
     * @param nameRe The Regular Expression string to identify the restrict value.
     */
    getForbiddenValueValidator(nameRe: RegExp): ValidatorFn {
        return (control: AbstractControl): { [key: string]: any } => {
            const forbidden = nameRe.test(control.value);
            return forbidden ? { 'forbiddenValue': { forbiddenValue: nameRe.source, value: control.value } } : null;
        }
    }

    /**
     * Gets the getInvalidFormatValidator to check whether the input is in the specified format.
     * @param nameRe The Regular Expression string to identify the restrict value.
     */
    getInvalidFormatValidator(nameRe: RegExp): ValidatorFn {
        return (control: AbstractControl): { [key: string]: any } => {
            const format = nameRe.test(control.value);
            return format ? null : { 'invalidFormat': { invalidFormat: nameRe.source, value: control.value } };
        }
    }

    /**
     * Gets the ValueNotMatchValidator to check whether two input values are identical.
     * @param referencedFieldModel The reference field model whose value will be compared to fieldModel value.
     * @param fieldModel The current field model.
     */
    getValueNotMatchValidator(referencedFieldModel: NgModel | AbstractControl, fieldModel: NgModel | AbstractControl): ValidatorFn {
        return (control: AbstractControl): { [key: string]: any } => {
            const notMatched = referencedFieldModel.value !== fieldModel.value;

            // For Reactive Form Validation Example:
            // this.formGroup.controls.confirmPassword.setValidators([
            //     Validators.required,
            //     this.validatorFactory.getValueNotMatchValidator(this.formGroup.controls.password, this.formGroup.controls.confirmPassword)
            // ]);

            // BY NOW, we need another line of the code.
            // this.formGroup.controls.password.valueChanges.subscribe(() =>
            // this.validatorFactory.setValueNotMatchFieldModelError(this.formGroup.controls.password, this.formGroup.controls.confirmPassword));

            // BY NOW, I don't think each fieldModel value change applies one time subscribtion is a good idea. Before I can avoid to subscribe 
            // multiple times I leave it here. Maybe I can figure it out later.
            // Subscribes the valueChanges of the reference field model. If the reference field value changes also triggers current field's validity change.
            // referencedFieldModel.valueChanges.subscribe(() => this.setValueNotMatchFieldModelError(referencedFieldModel, fieldModel));

            return notMatched ? this.getValueNotMatchDefaultError(fieldModel.value) : null;
        };
    }

    /**
     * Sets the field model error for the ValueNotMatchValidator.
     * @param fieldModel The input field. The NgModel field for template-driven form or the AbstractControl for the reactive form.
     * @param fieldModelErrorToChange Another input field which value will be compared to the fieldModel value. The model error will be set based on the result.
     */
    setValueNotMatchFieldModelError(fieldModel: NgModel | AbstractControl, fieldModelErrorToChange: NgModel | AbstractControl): void {
        let notMatched = fieldModel.value !== fieldModelErrorToChange.value;

        if (notMatched) {
            // If two values are NOT identical.
            // Sets the valueNotMatch error on referencedFieldModel field if needed.
            if (!fieldModelErrorToChange.errors || (fieldModelErrorToChange.errors && !fieldModelErrorToChange.errors.valueNotMatch)) {
                if (fieldModelErrorToChange instanceof NgModel) {
                    fieldModelErrorToChange.control.setErrors(this.getValueNotMatchDefaultError(fieldModelErrorToChange.value));
                } else if (fieldModelErrorToChange instanceof AbstractControl) {
                    fieldModelErrorToChange.setErrors(this.getValueNotMatchDefaultError(fieldModelErrorToChange.value));
                }
            }
        } else {
            // If two values are identical, removes the valueNotMatch error from referencedFieldModel field.
            // Otherwise sets the valueNotMatch error on referencedFieldModel field.
            if (fieldModelErrorToChange.errors && fieldModelErrorToChange.errors.valueNotMatch) {
                // Removes the valueNotMatch error from referencedFieldModel field.
                delete fieldModelErrorToChange.errors.valueNotMatch;

                // If there's no more any other errors.
                if (fieldModelErrorToChange.errors !== {}) {
                    // Changes the referencedFieldModel field validity.
                    if (fieldModelErrorToChange instanceof NgModel) {
                        fieldModelErrorToChange.control.updateValueAndValidity();
                    } else if (fieldModelErrorToChange instanceof AbstractControl) {
                        fieldModelErrorToChange.updateValueAndValidity();
                    }
                }
            }
        }
    }

    /**
     * Gets the defaut error.
     * @param inputValue The form field input value.
     * @return An object contains property can be accessed via [fieldName].errors.[propertyName].
     */
    getValueNotMatchDefaultError(inputValue: any): any {
        return { 'valueNotMatch': { value: inputValue } };
    }
}